<script setup>
import * as PIXI from 'pixi.js';
import { h, computed, onBeforeUnmount, onMounted, provide, ref, watch, shallowRef } from 'vue';
import { SettingOutlined, InfoCircleOutlined } from '@ant-design/icons-vue';
import { Modal, message } from 'ant-design-vue';
import router from '@/router/index.js';
import { RndInt } from '@/util/rnd.js';
// import { readCV, useOnnx, SortOf, CanvasSizeOf, DrawBy } from './api/use-onnx.js';
import { useHotLabel } from './api/use-hot-label.js';
import TopBar from './components/top-bar.vue';
import LabelLayers from './components/label-layers.vue';
import LabelComments from './components/label-comments.vue';
import ToolBar from './components/tool-bar.vue';
import LabelSelector from './components/label-selector.vue';
import ImgCategs from './components/img-categs.vue';
import { DataLabel, InitAppOf } from './index.js';
import { AppOpts } from './common.js';
import { setIndex, useSwitchIndex } from './api/use-switch-index.js';
import { useKbdModal } from './kbd-modal.js';
import { useLayer } from './api/use-layer.js';
import { useComment } from './api/use-comment.js';
import { CustomShowConfirmNextPkg, CustomJudge, useJudge } from './api/use-judge.js';
import { useScale } from './api/use-scale.js';
import { useOpPerms } from './op-perms.js';
import { useTool } from './api/use-tool.js';
import * as storage from '@/util/storage.js';
import { REQU, getAssignedFileList } from '@/net/requ.js';
import { PkgListReadOf, isNewPatternClasses, toDic } from '@/util/index.js';
import { useShortcutKey } from './api/use-shortcut-key.js';
import { CustomSaveFile, toStorableOf } from './pkg-state.js';
import OverlimitDetail from './OverlimitDetail.jsx';

const props = defineProps(['mode']);
provide('props', props);

function useClasses(taskInfo) {
  const RndColor = () => `rgb(${ RndInt(0,256) },${ RndInt(0,256) },${ RndInt(0,256) })`;

  const labelsStr = computed(() => R.propOr('', 'labels', taskInfo.value));

  /** @type {function(string): string[]} */
  const toClasses = R.ifElse(
    isNewPatternClasses,
    R.compose( R.pluck('label'), R.curryN(1, JSON.parse) ),
    R.split(','),
  )

  const toLabelLimitDic = R.ifElse(
    isNewPatternClasses,
    R.compose( toDic(['label', 'limit']), R.curryN(1, JSON.parse) ),
    R.compose( toDic(['label', 'limit']), R.map(label => ({ label, limit: 100 })), R.split(',') )
  )

  const labelLimitDic = computed(() => toLabelLimitDic(labelsStr.value));

  // const classes = computed(() => ['手','铁架台','支架','弹簧测力计','弹簧测力计拉杆','弹簧测力计外壳','金属块','头部','细线','盛有水的烧杯','抹布','操作区','弹簧测力计挂在支架上','弹簧测力计下端挂有金属块','金属块浸入水中','抹布擦拭实验台','读取弹簧测力计示数']);
  const classes = computed(() => toClasses(labelsStr.value));
  
  // 之前存标签下标，现在成存标签内容
  /** 具备兼容性的 classes，用于读下标和标签内容 */
  const compatibleClasses = computed(() => (
    R.compose(
      R.converge(R.mergeDeepRight, [
        R.identity,
        R.compose( R.fromPairs, R.map(R.repeat(R.__, 2)) )
      ]),
      toClasses
    )(labelsStr.value)
  ));

  const longestLabelLength = computed( () => Math.max(...R.map(R.prop('length'), classes.value)) );

  const labelColorDic = computed(() => (
    R.converge(R.zipObj, [
      R.identity,
      R.compose( R.times(RndColor), R.length )
    ])(classes.value)
  ));

  const classesSorted = ref(R.clone(classes.value));
  const imageClasses = computed(() => R.compose( R.split(','), R.propOr('', 'imageCategories') )(taskInfo.value));
  const compatibleImageClasses = computed(() => R.converge(R.mergeDeepRight, [
    R.identity,
    R.compose( R.fromPairs, R.map(R.repeat(R.__, 2)) )
  ])(imageClasses.value));
  
  return {
    classes,
    compatibleClasses,
    longestLabelLength,
    labelColorDic,
    classesSorted,
    imageClasses,
    compatibleImageClasses,
    labelLimitDic,
  };
}

function main() {
  taskInfo.value = storage.readAsObj('taskInfo') || {
    labels: '砝码,天平,托盘,操作区,抹布,镊子,试管,试管刷',
  };
  
  // 避免因初始化触发 watch
  if ('search' == props.mode && searchParams.value.taskIds != taskInfo.value.id) {
    searchParams.value.taskIds = taskInfo.value.id
  }

  // if (!taskInfo.value) {
  //   message.error('请先选择任务');
  //   router.push({ path: '/task' });
  //   return;
  // }

  onMounted(() => {
    DataLabel({
      addAllowDefault, addPreCond, listenShortcutKey, addShortcutKey, handleShortcutKey,
      textures, img,
      changed, saveFile, judge, judgePassQues, handleReject,
      initApp, app,
      classes, compatibleClasses, longestLabelLength, imageClasses,
      overlimitModalProps, OverlimitSatisfies, OverlimitCountForQues,
      LabelCountPairsForExtr, LabelCountPairsForQues,
      labelColorDic, appOpts, taskInfo,
      kbdModalProps,
      quesList, quesIndex, quesSwitchable,
      ques, layers, comments, categories,
      lastSelectIndex, selectSwitchable,
      OpEq, OpGte, addPerms,
      percentage, minPercentage, maxPercentage, scaleChangeTo, calcOnPct, calcPctAtContain, elemPosInCenterOfScreen,
      using, isUsing, isUsingTool, isNotUsingTool,
      arrowAction, editingLayer, activeLayer, selectedLayers, copiedLayers, autoAppendLayersExpected,
      delLayer, toggleVisibleOfLayers, selectLayer, findSibling, cloneLayer, copyLayers, pasteLayers, kbdMove,
      editingComment, selectComment,
      confirmModalProps, judgeModalProps, useJudgeModal, commentModalProps,
      toggleUsingDrag, labelSelectorProps,
    })
    .then(() => {
      watch(
        searchParams,
        () => {
          storage.writeAsStr('ques-search', searchParams.value);
          initApp(storage.readAsObj('pkgInfo'));
        },
        { deep: true }
      );
    });
  });

  onBeforeUnmount(() => {
    app.destroy();
    removeEventListener('keydown', handleShortcutKey);
  });
}

const { addAllowDefault, addPreCond, listenShortcutKey, addShortcutKey, handleShortcutKey } = useShortcutKey();

const loading = ref(/* true */ false);
provide('loading', loading);

/** @type {import('vue').ShallowRef<PIXI.Texture[]>} */
const textures = shallowRef([]);

/** @type {import('vue').ShallowRef<PIXI.Sprite>} */
const img = shallowRef(new PIXI.Sprite());

const app = new PIXI.Application();
const appOpts = ref(AppOpts());
const changed = ref(false);

/** @type {import('vue').Ref<ObjDetn.TaskInfo>} */
const taskInfo = ref(null);
provide('taskInfo', taskInfo);

const searchParams = ref(storage.readAsObj('ques-search')
  || {
    taskIds: taskInfo.value?.id,
    labelName: '',
    cateName: '',
    orderField: 'submit_time',
    orderType: 'DESC',
    queryString: '',
    start: 1,
    limit: 10,
  }
);
provide('searchParams', searchParams);

const quesTotal = ref(Number(storage.read('quesTotal')));
provide('quesTotal', quesTotal);

const { classes, compatibleClasses, longestLabelLength, labelColorDic, /* classesSorted, */ imageClasses, compatibleImageClasses, labelLimitDic } = useClasses(taskInfo);
provide('classes', classes);
provide('labelColorDic', labelColorDic);
provide('imageClasses', imageClasses);

const labelSelectorProps = ref({ visible: false, loading: false });

const { OpEq, OpGte, addPerms } = useOpPerms(storage.read('operate') || /* '查看' */ '标注');
provide('OpEq', OpEq);
provide('OpGte', OpGte);

const { using, isUsing, isUsingTool, isNotUsingTool } = useTool();
provide('using', using);
provide('isUsing', isUsing);
provide('isUsingTool', isUsingTool);
provide('isNotUsingTool', isNotUsingTool);

function toggleUsingDrag() {
  using.value = isUsing('drag') ? 'none' : 'drag';
}

provide('toggleUsingDrag', toggleUsingDrag);

const kbdModalProps = useKbdModal();
provide('kbdModalProps', kbdModalProps);

/** @type {import('vue').Ref<ObjDetn.Ques[]>} */
const quesList = ref([]);
const { i: quesIndex, switchable: quesSwitchable } = useSwitchIndex(quesList);
provide('quesList', quesList);
provide('quesIndex', quesIndex);
provide('quesSwitchable', quesSwitchable);

const ques = computed(() => quesList.value[quesIndex.value]);
provide('ques', ques);

const layers = computed({
  get: () => ques.value?.assignedLabels ?? [],
  set: v => ques.value.assignedLabels = v,
});
provide('layers', layers);

const comments = computed({
  get: () => ques.value?.comments ?? [],
  set: v => ques.value.comments = v,
});
provide('comments', comments);

const categories = computed({
  get: () => ques.value?.assignedImageCategories ?? [],
  set: v => ques.value.assignedImageCategories = v,
});
provide('categories', categories);

const { i: lastSelectIndex, switchable: selectSwitchable } = useSwitchIndex(layers);
provide('lastSelectIndex', lastSelectIndex);
provide('selectSwitchable', selectSwitchable);

const {
  arrowAction,
  editingDataItem: editingLayer,
  activeDataItem: activeLayer,
  selectedDataItems: selectedLayers,
  copiedDataItems: copiedLayers,
  autoAppendDataItemsExpected: autoAppendLayersExpected,
  clear: clearLayers,
  del: delLayer,
  toggleVisible: toggleVisibleOfLayers,
  select: selectLayer,
  findSibling,
  clone: cloneLayer,
  copy: copyLayers,
  paste: pasteLayers,
  setExpectedAutoAppend,
  kbdMove,
} = useLayer(layers, lastSelectIndex);
provide('arrowAction', arrowAction);
provide('editingLayer', editingLayer);
provide('activeLayer', activeLayer);
provide('selectedLayers', selectedLayers);
provide('autoAppendLayersExpected', autoAppendLayersExpected);
provide('clearLayers', clearLayers);
provide('delLayer', delLayer);
provide('toggleVisibleOfLayers', toggleVisibleOfLayers);
provide('selectLayer', selectLayer);
provide('findSibling', findSibling);
provide('cloneLayer', cloneLayer);
provide('copyLayers', copyLayers);
provide('pasteLayers', pasteLayers);
provide('setExpectedAutoAppend', setExpectedAutoAppend);

const {
  editingDataItem: editingComment,
  select: selectComment,
  clear: clearComments,
  del: delComment,
  toggleVisible: toggleVisibleOfComments,
} = useComment(comments);
provide('editingComment', editingComment);
provide('selectComment', selectComment);
provide('clearComments', clearComments);
provide('delComment', delComment);
provide('toggleVisibleOfComments', toggleVisibleOfComments);

const { judgeModalProps, useJudgeModal, confirmModalProps, commentModalProps } = useJudge();
provide('judgeModalProps', judgeModalProps);
provide('useJudgeModal', useJudgeModal);
provide('confirmModalProps', confirmModalProps);
provide('commentModalProps', commentModalProps);

const { percentage, minPercentage, maxPercentage, scaleChangeTo, calcOnPct, calcPctAtContain, elemPosInCenterOfScreen } = useScale(app);
provide('percentage', percentage);
provide('minPercentage', minPercentage);
provide('maxPercentage', maxPercentage);
provide('scaleChangeTo', scaleChangeTo);

function getSearchedFileList(params) {
  return REQU(`/api/taskAssignmentPackageFile/list?${ new URLSearchParams(params) }`).then(R.path(['data', 'list']));
}

const pkgList = ref(R.compose( PkgListReadOf, R.prop('user'), storage.readAsObj )('pkgInfo'));

/** 此变量变化，带动更新 storage */
const pkgInfoInStorage = ref(storage.readAsObj('pkgInfo'));
provide('pkgInfoInStorage', pkgInfoInStorage);

const pkgIndex = computed(() => {
  const findIndex = R.findIndex(R.propEq( pkgInfoInStorage.value.pkg, 'id' ));
  return findIndex(pkgList.value);
});

const switchPkgAbleToPrev = computed(() => 0 < pkgIndex.value);
provide('switchPkgAbleToPrev', switchPkgAbleToPrev);
const switchPkgAbleToNext = computed(() => pkgIndex.value + 1 < pkgList.value.length);
provide('switchPkgAbleToNext', switchPkgAbleToNext);

const initApp = InitAppOf({
  getFileList: (
    'search' == props.mode
    ? () => getSearchedFileList(searchParams.value)
    : pkgInfo => getAssignedFileList(pkgInfo.pkg).then(R.prop('data'))
  ),
  loading,
  quesIndex,
  img,
  textures,
  classes,
  compatibleClasses,
  imageClasses,
  compatibleImageClasses,
  labelColorDic,
  quesList,
  autoAppendLayersExpected,
});
provide('initApp', initApp);

const saveFile = CustomSaveFile({
  cond: () => OpGte('标注') && changed.value,
  pre: ques => {
    ques.assignedLabels = R.filter(R.compose( isValidLabel, R.prop('label') ), ques.assignedLabels);
  },
  Params: ques => ({
    source: R.prop(storage.read('operate') || '查看', { '标注': 1, '查看': 2, '审核': 3 }),
    auditStatus: OpGte('审核') ? ques.auditStatus : null,
    assignedFileId: ques.assignedFileId,
    assignedLabels: R.compose( R.join(','), R.map(toStorableOf({ compatibleClasses, img })) )(ques.assignedLabels),
    assignedImageCategories: R.compose( R.join(','), /* R.map(R.indexOf(R.__, imageClasses.value)) */ )(ques.assignedImageCategories),
    assignedLabelsText: R.compose( R.join(','), R.uniq, R.unnest, R.pluck('label') )(ques.assignedLabels),
    assignedImageCategoriesText: R.join(',', ques.assignedImageCategories),
  }),
  Msg: R.always(`第 ${ quesIndex.value + 1 } 题保存失败`),
});
provide('saveFile', saveFile);

const showConfirmNextPkg = CustomShowConfirmNextPkg({
  confirmModalProps,
  taskInfo,
  switchable: () => switchPkgAbleToNext.value,
  toNextPkg: value => {
    pkgInfoInStorage.value = value;
    initApp(value)
  },
});

// 判断审题还是审题包
// 若审题则改变此题状态后跳到下一题
//   若跳不动提示则判断，是否在最后一包
//     若是则提示最后一包审完，只能返回任务详情
//     否则确认是否下一包
// 若审题包则改变整包题状态后，跳下一包
//   判断是否可跳下一包
//     若是则提示最后一包审完，只能返回任务详情
//     否则确认是否下一包
const judge = CustomJudge({
  pre: () => confirmModalProps.value.visible = true,
  Params: params => {
    const NamesInQues = R.ifElse(
      R.has('wholePackagePass'),
      R.compose(
        R.pick(R.__, ques.value),
        R.always(['packageId'])
      ),
      R.compose(
        R.mergeRight({ lastFile: !quesSwitchable.value.next }),
        R.pick(R.__, ques.value),
        R.always(['assignedFileId', 'packageId'])
      )
    );

    const CommentsToStr = R.ifElse( R.length, R.curryN(1, JSON.stringify), R.always('') );
    return R.compose(
      R.mergeRight({ note: CommentsToStr(comments.value) }),
      NamesInQues,
    )(params);
  },
  quesJudged: params => {
    ques.value.auditStatus = params.pass ? 1 : 2;
    !params.pass && Object.assign(ques.value, R.pick(['rejectReason', 'remark' ], params));

    if (quesSwitchable.value.next) {
      setIndex(quesIndex, 'next');
      confirmModalProps.value.visible = false;
    } else {
      saveFile(ques.value);
      showConfirmNextPkg('已审核至最后一题');
    }
  },
  pkgJudged: params => {
    quesList.value.forEach(item => (item.auditStatus = params.wholePackagePass ? 1 : 2));
    saveFile(ques.value);
    showConfirmNextPkg('该包审核完毕');
  },
});
provide('judge', judge);

const judgePassQues = () => {
  const dataSource = OverlimitSatisfies( R.gt(R.__, 0), OverlimitCountForQues.value, LabelCountPairsForQues )(layers.value);

  if (0 == dataSource.length) {
    judge({ pass: true });
    return;
  }

  overlimitModalProps.value.visible = true;
  Modal.confirm({
    width: 1000,
    centered: true,
    title: '标签超量',
    content: OverlimitDetail(
      { dataSource },
      { slots: { description: () => '本题有标签超出限量，仍要通过吗？详情如下：' } }
    ),
    okText: '通过',
    onOk: () => {
      overlimitModalProps.value.visible = false;
      judge({ pass: true });
    },
    cancelText: '再看看',
    onCancel: () => (overlimitModalProps.value.visible = false),
  });
};

const judgePassPkg = () => {
  const isValidLabelCountInPkg = R.all(R.compose(
    R.propEq(0, 'length'),
    OverlimitSatisfies( R.gt(R.__, 0), OverlimitCountForPkg.value, LabelCountPairsForExtr ),
    R.prop('assignedLabels')
  ));

  if (isValidLabelCountInPkg(quesList.value)) {
    confirmModalProps.value.visible = true;
    Modal.confirm({
      centered: true,
      content: '确定要整包通过吗？',
      okText: '通过',
      onOk: () => judge({ wholePackagePass: true, remark: '' }),
      cancen: '再看看',
      onCancel: () => confirmModalProps.value.visible = false,
    });
    return;
  }

  overlimitModalProps.value.visible = true;
  Modal.confirm({
    centered: true,
    title: '标签超量',
    content: '本包某些题的标签超量',
    okText: '通过',
    onOk: () => {
      overlimitModalProps.value.visible = false;
      judge({ wholePackagePass: true, remark: '' });
    },
    cancelText: '再看看',
    onCancel: () => (overlimitModalProps.value.visible = false),
  });
};

provide('judgePassQues', judgePassQues);
provide('judgePassPkg', judgePassPkg);

const handleReject = () => (
  comments.value.length
  ? judge({ pass: false })
  : useJudgeModal()
);
provide('handleReject', handleReject);

const appOptsModalProps = ref({ visible: false });
watch(
  () => appOptsModalProps.value.visible,
  newVal => (app.stage.eventMode = newVal ? 'none' : 'static')
);

// operate, pkgInfo 临时写个假的
storage.write('operate', '标注');
storage.writeAsStr('pkgInfo', { user: 1, pkg: 1, ques: 1 });

main();

// --- taskInfo.value 已赋值，依赖它的已更新 ---

// 目前为了标注任务，label 皆为 length=1 的数组
const isInClasses = R.includes(R.__, classes.value);
const isSelected = R.compose( R.gt(R.__, 0), R.length );
const isValidLabel = R.allPass([ isSelected, R.all(isInClasses) ]);
const LabelsIn = R.compose( R.unnest, R.filter(isValidLabel), R.pluck('label') );
const LabelsInQues = R.compose( LabelsIn, R.prop('assignedLabels') );
const LabelsInPkg = R.transduce( R.map(LabelsInQues), R.concat, [] );

const labelCountDic = computed(() => {
  const DefaultDic = () => R.compose( R.fromPairs, R.map(x => [x, 0]) )(classes.value);
  return R.ifElse(
    Boolean,
    R.compose( R.mergeRight(DefaultDic()), R.countBy(R.identity), LabelsInQues ),
    DefaultDic,
  )(ques.value)
});

/** 已选标签数量小于等于限量视为正常 */
const isValidLabelCount = computed(() => (
  R.converge(R.lte, [
    R.prop(R.__, labelCountDic.value),
    R.prop(R.__, labelLimitDic.value)
  ])
));

const OverlimitCountForQues = computed(() => ({ label, count }) => count + labelCountDic.value[label] - labelLimitDic.value[label]);
const OverlimitCountForPkg = computed(() => ({ label, count }) => count - labelLimitDic.value[label]);
const calcOverlimitCount = calc => ([label, count]) => [label, Math.max(0, calc({ label, count }))];

const LabelCountPairsForExtr = R.compose( R.toPairs, R.countBy(R.identity) )
const LabelCountPairsForQues = R.compose( R.map(label => [label, 0]), R.uniq )
/** Layer[] -> Label[] -> { Label: CountByLabel } -> [[Label, CountByLabel]] -> [[Label, OverlimitCount]] */
const OverlimitCountTable = (calc, LabelCountPairs) => R.compose( R.map(calcOverlimitCount(calc)), LabelCountPairs, LabelsIn );
const OverlimitSatisfies = (detect, calc, LabelCountPairs) => R.compose( R.filter(R.compose( detect, R.last )), OverlimitCountTable(calc, LabelCountPairs) );

const validateLabels = R.compose( R.isEmpty, OverlimitSatisfies(R.gt(R.__, 0)) );

/** 已选标签数量小于限量视为可选 */
const isLabelSelectable = computed(() => (
  R.converge(R.lt, [
    R.prop(R.__, labelCountDic.value),
    R.prop(R.__, labelLimitDic.value),
  ])
));
provide('isLabelSelectable', isLabelSelectable);

const overlimitModalProps = ref({ visible: false });
function labelCountOverlimitWarn(label) {
  overlimitModalProps.value.visible = true;
  Modal.warning({
    class: 'label-limit',
    centered: true,
    title: `限量 ${labelLimitDic.value[label]} 个`,
    content: h('p', `请检查当前图片已标注的「${label}」\n如有疑问请联系审核员或管理员。`),
    onOk: () => (overlimitModalProps.value.visible = false)
  });
}

provide('labelCountOverlimitWarn', labelCountOverlimitWarn);

const classesSorted = useHotLabel({ appOpts, classes, LabelsInQues, LabelsInPkg, quesList, ques });

// 若 cv 预标注模型链接有效，则标签以概率排序
// const sortClasses = R.ifElse(
//   Boolean,
//   url => {
//     const getIndexListSorted = useOnnx(new onnx.InferenceSession(), url);

//     return ({ imgElem, dataItem }) => {
//       labelSelectorProps.value.loading = true;
//       R.compose( getIndexListSorted, DrawBy(imgElem, dataItem), CanvasSizeOf )(dataItem)
//       .then(SortOf(classes.value))
//       .then(xs => (classesSorted.value = xs))
//       .finally(() => (labelSelectorProps.value.loading = false));
//     }
//   },
//   () => R.identity
// )(readCV(R.prop('modelUrl', taskInfo.value)) /* '/data-label/model-116.onnx' */);
</script>

<template>
  <div class="base-label obj-detn">
    <a-spin :spinning="loading">
      <top-bar></top-bar>
      <tool-bar></tool-bar>
      <div id="workspace"></div>
      <div class="data-bar">
        <label-layers></label-layers>
        <label-comments></label-comments>
        <img-categs></img-categs>
      </div>
  
      <footer>
        <a-popover
          title="设置"
          trigger="click"
          overlayClassName="base-label-popover"
          placement="topLeft"
          @visible-change="visible => (appOptsModalProps.visible = visible)">
          <template #content>
            <a-form :label-col="{ span: 13 }">
              <a-form-item>
                <template #label>
                  <a-tooltip placement="topLeft">
                    <template #title>
                      [选择标签] 窗口的标签列表<strong>自动</strong>排序
                    </template>
                    标签智能排序
                  </a-tooltip>
                </template>
                <a-checkbox v-model:checked="appOpts.autoLabelSort"></a-checkbox>
              </a-form-item>

              <a-form-item label="标注后选中">
                <a-checkbox v-model:checked="appOpts.selectNewly"></a-checkbox>
              </a-form-item>
          
              <a-form-item label="控制点半径">
                <a-input-number
                  :value="appOpts.ctrlPoint.r"
                  :min="0"
                  @pressEnter="event => appOpts.ctrlPoint.r = Number.parseInt(event.target.value)"
                  @step="value => appOpts.ctrlPoint.r = value"
                  @change="value => appOpts.ctrlPoint.r = Number.parseInt(value)"
                >
                </a-input-number>
              </a-form-item>
          
              <a-form-item label="醒目块不透明度">
                <a-input-number
                  :value="appOpts.eyeCatcher.alpha"
                  :max="1"
                  :min="0"
                  :step=".01"
                  @pressEnter="event => appOpts.eyeCatcher.alpha = Number.parseFloat(event.target.value.toFixed(2))"
                  @step="value => appOpts.eyeCatcher.alpha = value"
                  @change="value => appOpts.eyeCatcher.alpha = Number.parseFloat(value.toFixed(2))"
                >
                </a-input-number>
              </a-form-item>
            </a-form>
          </template>
          <a-button title="设置">
            <template #icon><setting-outlined /></template>  
            设置
          </a-button>
        </a-popover>
  
        <a-popover
          title="任务详情"
          overlayClassName="base-label-popover"
          placement="topLeft"
          trigger="click">
          <template #content>
            <div>任务名称：{{ taskInfo.taskName }}</div>
            <div>文件名称：{{ ques?.fileName ?? '--' }}</div>
          </template>
          <a-button title="任务详情">
            <template #icon><info-circle-outlined /></template>
            任务详情
          </a-button>
        </a-popover>
  
        题ID：{{ ques?.fileId ?? '--' }}
      </footer>
    </a-spin>
  </div>

  <label-selector
    v-if="labelSelectorProps.visible"
    v-model:visible="labelSelectorProps.visible"
    :loading="labelSelectorProps.loading"
    :classes="classesSorted"
    @cancel="() => {
      delLayer(activeLayer);
      activeLayer = null;
    }"
    @ok="label => {
      if (!isLabelSelectable(label)) {
        labelCountOverlimitWarn(label);
        return;
      }

      // 之前想支持标签多选，后来又改成单选，为兼顾之前接口数据，label 依然为数组
      const xs = Array.of(label);
      activeLayer.label = xs;
      activeLayer.color = labelColorDic[xs[0]];
      appOpts.selectNewly ? selectLayer('single', 'switch', activeLayer) : (activeLayer = null);
      labelSelectorProps.visible = false;
    }">
  </label-selector>
</template>

<style>
.base-label {
  min-width: 1440px;
  max-width: 1920px;
  height: 100%;
}

.base-label .ant-spin-nested-loading > div > .ant-spin { position: static; }

.base-label .ant-spin-nested-loading,
.base-label .ant-spin-container { height: 100%; }

.base-label .ant-spin-container {
  display: grid;
  grid-template-areas:
    'header header header'
    'toolbar . databar'
    'footer footer footer';
  grid-template-rows: auto 1fr auto;
  grid-template-columns: 60px 1fr 240px;
  overflow: hidden;
  user-select: none;
}

.base-label .ant-btn { padding: 0 .5em; }
.base-label .ant-btn.ant-btn-icon-only {
  padding-left: 0;
  padding-right: 0;
}

.base-label .ant-spin-container > .data-bar {
  grid-area: databar;
  display: flex;
  flex-direction: column;
  background: #fff;
}

#workspace { position: relative; }
#workspace > canvas { position: absolute; }

.base-label .ant-spin-container > footer {
  grid-area: footer;
  position: relative;
  display: flex;
  align-items: center;
  gap: .5em;
  height: max-content;
  padding: .5em;
  border-top: 1px solid rgb(228,228,228);
}

.base-label .ant-spin-container > footer .ant-btn {
  height: 30px;
  padding-left: .5em;
  padding-right: .5em;
}

.base-label-popover .ant-popover-arrow { display: none; }

.label-limit .ant-modal-confirm-content { word-break: keep-all; }
.label-limit .ant-modal-confirm-content p { margin-bottom: 0; }
</style>
